這篇開始會進入實作「標籤輸入」的系列,我已經建立一個基礎的程式架構,簡單來說新增兩個 UIViewController,也就是兩個介面,第一個介面是之後我們顯示「帳」的列表,第二個介面是我們的主角「快速記帳」。
請參考 GitHub。
基於這個程式架構,我們接下來會一一實作「標籤輸入」的程式。
首先我們先實作稍後會用到的兩種 UICollectionViewCell。
先簡單的在裡面放一個 UILabel,我們之後會一起調整內容顯示的邏輯。
class TagCollectionViewCell: UICollectionViewCell {
    lazy var label: UILabel = {
        return UILabel()
    }()
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(label: label)
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    private func addSubview(label: UILabel) {
        contentView.addSubview(label)
        let margin = contentView.layoutMarginsGuide
        label.translatesAutoresizingMaskIntoConstraints = false
        label.leftAnchor.constraint(equalTo: margin.leftAnchor).isActive = true
        label.rightAnchor.constraint(equalTo: margin.rightAnchor).isActive = true
        label.topAnchor.constraint(equalTo: margin.topAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: margin.bottomAnchor).isActive = true
    }
}
一樣先在裡面放一個 UITextField,之後再一起調整排版的邏輯。
須注意的是,我們要監控 UITextField 按下 Return 的事件,代表使用者輸入完標籤後,確定要建立該標籤。
因此,我們需要先定義一個 Delegate Protocol:
protocol TagTextFieldDelegate {
    func didAdd(tag: String)
}
接著實作 TagTextFieldCollectionViewCell:
class TagTextFieldCollectionViewCell: UICollectionViewCell {
    lazy var textField: UITextField = {
        var textField = UITextField()
        textField.delegate = self
        return textField
    }()
    var delegate: TagTextFieldDelegate?
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(textField: textField)
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    private func addSubview(textField: UITextField) {
        contentView.addSubview(textField)
        let margin = contentView.layoutMarginsGuide
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.leftAnchor.constraint(equalTo: margin.leftAnchor).isActive = true
        textField.rightAnchor.constraint(equalTo: margin.rightAnchor).isActive = true
        textField.topAnchor.constraint(equalTo: margin.topAnchor).isActive = true
        textField.bottomAnchor.constraint(equalTo: margin.bottomAnchor).isActive = true
    }
}
只要 UITextField 內有文字,並按下 Return 鍵後,我們就把目前 UITextField 內的文字取出來,當做標籤丟給 TagTextFieldDelegate,如下:
extension TagTextFieldCollectionViewCell: UITextFieldDelegate {
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        guard let text = textField.text, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            return false
        }
        delegate?.didAdd(tag: text.trimmingCharacters(in: .whitespacesAndNewlines))
        textField.text = ""
        return true
    }
}
稍後我們就可以在 UIViewController 利用這個 Delegate,取得使用者建立的標籤。
首先我們先在介面上加入 UICollectionView,給個大概的高度,先填上背景色方便我們辨識,並註冊前面實作的兩種 UICollectionViewCell:
class QuickCreateViewController: UIViewController {
    let tagCollectionView: UICollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout())
    var tags: [String] = []
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = MMColor.white
        addSubview(tagCollectionView: tagCollectionView)
    }
    private func addSubview(tagCollectionView: UICollectionView) {
        view.addSubview(tagCollectionView)
        tagCollectionView.translatesAutoresizingMaskIntoConstraints = false
        tagCollectionView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        tagCollectionView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        tagCollectionView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor).isActive = true
        tagCollectionView.heightAnchor.constraint(equalToConstant: 44 * 3).isActive = true
        tagCollectionView.backgroundColor = MMColor.black
        tagCollectionView.dataSource = self
        tagCollectionView.delegate = self
        tagCollectionView.register(TagCollectionViewCell.self, forCellWithReuseIdentifier: NSStringFromClass(TagCollectionViewCell.self))
        tagCollectionView.register(TagTextFieldCollectionViewCell.self, forCellWithReuseIdentifier: NSStringFromClass(TagTextFieldCollectionViewCell.self))
    }
}
並且先簡單的加上顯示 UICollectionViewCell 的邏輯:
如下:
extension QuickCreateViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return tags.count + 1
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        if indexPath.row == tags.count {
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(TagTextFieldCollectionViewCell.self), for: indexPath) as? TagTextFieldCollectionViewCell else {
                fatalError()
            }
            cell.backgroundColor = MMColor.red
            cell.delegate = self
            return cell
        } else {
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(TagCollectionViewCell.self), for: indexPath) as? TagCollectionViewCell else {
                fatalError()
            }
            cell.backgroundColor = MMColor.white
            cell.label.text = tags[indexPath.row]
            return cell
        }
    }
}
接著加上 TagTextFieldDelegate 的實作,一但使用者在輸入框內按下 Return,我們取得標籤後,加入到陣列裡,並請 UICollectionView 重新讀取資料:
extension QuickCreateViewController: TagTextFieldDelegate {
    func didAdd(tag: String) {
        tags.append(tag)
        tagCollectionView.reloadData()
    }
}
最後,我們需要監控 UICollectionView 重新顯示 Cell 的事件,並在 UICollectionView 重新顯示資料時,再把 TagTextFieldCollectionViewCell 裡面的 UITextField 重新設為 First Responder,否則每次使用者新增完一個 Tag,並重新顯示 UICollectionView 時,鍵盤都會隱藏起來,就不能快速地連續新增標籤。
extension QuickCreateViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        (cell as? TagTextFieldCollectionViewCell)?.textField.becomeFirstResponder()
    }
}
至此,完整程式碼請看 GitHub。

下一篇我們將針對 UICollectionView 調整排版。
備註:有些地方用了 lazy var,但其實沒必要,之後預計會有一篇重構,到時候再來處理這些誤用。
做個實驗一下
似乎 force refresh 頁面個幾次之後就會有高亮了
class TagCollectionViewCell: UICollectionViewCell {
    lazy var label: UILabel = {
        return UILabel()
    }()
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(label: label)
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    private func addSubview(label: UILabel) {
        contentView.addSubview(label)
        let margin = contentView.layoutMarginsGuide
        label.translatesAutoresizingMaskIntoConstraints = false
        label.leftAnchor.constraint(equalTo: margin.leftAnchor).isActive = true
        label.rightAnchor.constraint(equalTo: margin.rightAnchor).isActive = true
        label.topAnchor.constraint(equalTo: margin.topAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: margin.bottomAnchor).isActive = true
    }
}
                                    我強制重整還是沒有欸